/**
 * Copyright (C) 2010 altuure <altuure [AT] gmail [DOT] com> http://www.altuure.com/projects/yagdao
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.altuure.yagdao.jpa;

import com.altuure.yagdao.*;
import com.altuure.yagdao.common.Utils;

import javax.persistence.EntityManager;
import javax.persistence.Query;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.*;
import javax.persistence.metamodel.EntityType;
import javax.persistence.metamodel.Metamodel;
import javax.persistence.metamodel.SingularAttribute;
import java.io.Serializable;
import java.util.*;

public class CommonJpaDAO implements CommonDAO {
    private EntityManagerAccessor entityManagerAccessor;



    public void setEntityManagerAccessor(EntityManagerAccessor entityManagerAccessor) {
        this.entityManagerAccessor = entityManagerAccessor;

    }

    /*
     * (non-Javadoc)
     * 
     * @see com.altuure.yagdao.GenericDAO#save(java.lang.Object)
     */

    public void save(Object entity) {
        getEntityManager().persist(entity);
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.altuure.yagdao.GenericDAO#load(java.io.Serializable)
     */

    public Object load(Class objectClass, Serializable id) {
        return (Object) getEntityManager().getReference(objectClass, id);
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.altuure.yagdao.GenericDAO#getAll()
     */

    public SearchResultList getAll(Class objectClass) {
        return getAll(objectClass, null);

    }

    /*
     * (non-Javadoc)
     * 
     * @see com.altuure.yagdao.GenericDAO#getAll(com.altuure.yagdao.DAOPage)
     */

    public SearchResultList getAll(Class objectClass, YPage paging) {
        EntityManager entityManager = getEntityManager();
        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
        CriteriaQuery<Object> criteriaQuery = criteriaBuilder.createQuery();
        Root from = criteriaQuery.from(objectClass);
        CriteriaQuery<Object> select = criteriaQuery.select(from);
        return executeQuery(select, paging, criteriaBuilder, from);

    }

    /*
     * (non-Javadoc)
     * 
     * @see com.altuure.yagdao.GenericDAO#get(java.io.Serializable)
     */

    public Object get(Class objectClass, Serializable Serializable) {

        return (Object) getEntityManager().find(objectClass, Serializable);
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.altuure.yagdao.GenericDAO#fetch(java.io.Serializable)
     */

    public Object fetch(Class objectClass, Serializable Serializable) {
        YFetch daoFetch = (YFetch) objectClass.getAnnotation(YFetch.class);
        CriteriaBuilder criteriaBuilder = getEntityManager().getCriteriaBuilder();

        CriteriaQuery<Object> criteriaQuery = criteriaBuilder.createQuery();
        Root from = criteriaQuery.from(objectClass);

        CriteriaQuery<Object> select = addFetchStatement(criteriaQuery, daoFetch, from);
        criteriaQuery.where(criteriaBuilder.equal(from.get(getIdProperty(objectClass)), Serializable));

        TypedQuery<Object> typedQuery = getEntityManager().createQuery(select);
        return (Object) typedQuery.getSingleResult();
    }

    /**
     * @param criteriaQuery root criteria
     * @param daoFetch      fetch properties
     * @param from          root
     * @return created CriteriaQuery
     */
    private CriteriaQuery<Object> addFetchStatement(CriteriaQuery<Object> criteriaQuery, YFetch daoFetch, Root from) {
        CriteriaQuery<Object> select = criteriaQuery.select(from);

        if (daoFetch != null) {
            String[] strings = daoFetch.value();
            for (String s : strings) {
                String[] split = s.trim().split(" ");
                if(split.length==1)
                    from.fetch(s);
                else
                    from.fetch(split[1], JoinType.valueOf(split[0].toUpperCase()));
            }

        }
        return select;
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.altuure.yagdao.GenericDAO#count()
     */

    public long count(Class objectClass) {
        Number count = (Number) getEntityManager().createQuery(
                "select count(*) from " + objectClass.getName() + " s ").getSingleResult();

        return count.longValue();
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.altuure.yagdao.GenericDAO#update(java.lang.Object)
     */

    public void update(Object object) {
        getEntityManager().merge(object);

    }

    /*
     * (non-Javadoc)
     * 
     * @see com.altuure.yagdao.GenericDAO#deleteById(java.io.Serializable)
     */

    public void deleteById(Class objectClass, Serializable serializable) {
        delete(load(objectClass, serializable));

    }

    /*
     * (non-Javadoc)
     * 
     * @see com.altuure.yagdao.GenericDAO#delete(java.lang.Object)
     */

    public void delete(Object object) {
        getEntityManager().remove(object);

    }

    /**
     * set limit for the query
     *
     * @param query       query
     * @param firstResult first result index
     * @param pageSize    page size
     */
    public void setLimits(Query query, int firstResult, int pageSize) {
        setLimits(query, new YLimit(firstResult, pageSize));
    }

    /**
     * set limits for query
     *
     * @param query query
     * @param limit page limits
     */
    public void setLimits(Query query, YLimit limit) {
        if (limit == null)
            return;
        if (limit.isPaged())
            query.setFirstResult(limit.getFirstResult());
        if (limit.isPageSized())
            query.setMaxResults(limit.getPageSize());
    }

    public SearchResultList<Object> query(Query query, YLimit paging) {
        setLimits(query, paging);
        return new SearchResultList<Object>(query.getResultList(), paging);
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.altuure.yagdao.GenericDAO#fetchAll()
     */

    public SearchResultList<Object> fetchAll(Class objectClass) {
        return fetchAll(objectClass, null);
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.altuure.yagdao.GenericDAO#fetchAll(com.altuure.yagdao.DAOPage)
     */

    public SearchResultList<Object> fetchAll(Class objectClass, YPage paging) {
        CriteriaBuilder criteriaBuilder = getEntityManager().getCriteriaBuilder();
        CriteriaQuery<Object> criteriaQuery = criteriaBuilder.createQuery();
        Root from = criteriaQuery.from(objectClass);
        YFetch daoFetch = (YFetch) objectClass.getAnnotation(YFetch.class);
        CriteriaQuery<Object> select = addFetchStatement(criteriaQuery, daoFetch, from);
        return executeQuery(select, paging, criteriaBuilder, from);

    }

    /**
     * execute query with given properties
     *
     * @param select          root query
     * @param paging          paging
     * @param criteriaBuilder root builder
     * @param from            parent from
     * @return executed list
     */
    private SearchResultList<Object> executeQuery(CriteriaQuery<Object> select, YPage paging,
                                                  CriteriaBuilder criteriaBuilder, Root from) {
        if (paging == null) {
            TypedQuery<Object> typedQuery = getEntityManager().createQuery(select);
            return new SearchResultList<Object>((List<Object>) typedQuery.getResultList());
        }

        setOrders(select, paging, criteriaBuilder, from, new HashMap());
        TypedQuery<Object> typedQuery = getEntityManager().createQuery(select);
        setLimits(typedQuery, paging);

        return new SearchResultList<Object>((List<Object>) typedQuery.getResultList(), paging);
    }

    /**
     * set Orders of given criteria.
     *
     * @param select          selct
     * @param paging          paging
     * @param criteriaBuilder criteria builder
     * @param from            root from
     * @param joins           joins map
     */
    public void setOrders(CriteriaQuery<Object> select, YPage paging, CriteriaBuilder criteriaBuilder, Root from,
                          Map<String, From> joins) {
        if (paging == null)
            return;
        if (paging.isOrdered()) {
            ArrayList<Order> jpaOrders = new ArrayList<Order>();
            List<YOrder> orders = paging.getOrders();
            for (YOrder daoOrder : orders) {

                Path path = extractPath(from, joins, daoOrder.getName());
                Order jpaOrder = (daoOrder.getSort().equals(YOrder.Sort.ASC)) ? criteriaBuilder.asc(path)
                        : criteriaBuilder.desc(path);
                jpaOrders.add(jpaOrder);
            }
            select.orderBy(jpaOrders.toArray(new Order[0]));

        }
    }

    /**
     * returns the Serializable property for given entity
     *
     * @return Serializable proeprty name
     */
    public String getIdProperty(Class objectClass) {
        String idProperty = null;
        Metamodel metamodel = getEntityManager().getMetamodel();
        EntityType entity = metamodel.entity(objectClass);
        Set<SingularAttribute> singularAttributes = entity.getSingularAttributes();
        for (SingularAttribute singularAttribute : singularAttributes) {
            if (singularAttribute.isId())
                idProperty = singularAttribute.getName();
        }

        return idProperty;
    }

    private EntityManager getEntityManager() {
        return entityManagerAccessor.getEntityManager();
    }

    protected Path extractPath(Root from, Map<String, From> joins, String property) {
        Path path;
        String[] strings = Utils.splitDot(property);

        From join = getLatestJoin(from, joins, strings);
        path = join.get(strings[strings.length - 1]);

        return path;
    }

    /**
     * create latest join from given root crietria from
     *
     * @param from    root
     * @param joins   join map cache
     * @param strings . splited field array
     * @return get latest join before property
     */
    protected From getLatestJoin(Root from, Map<String, From> joins, String[] strings) {
        // /not implemented yet

        From join = from;
        for (int j = 0; j < strings.length - 1; j++) {
            String propertyName = strings[j];
            String key = keyMaker(strings, j);

            if (joins.containsKey(key)) {
                join = joins.get(key);

            } else {
                if (j == 0) {
                    join = from.join(propertyName);
                } else {
                    join = join.join(propertyName);
                }
                joins.put(key, join);
            }

        }
        return join;
    }

    /**
     * create latest join from given root crietria from
     *
     * @param from    root
     * @param fetchs  fetch map cache
     * @param strings . splited field array
     * @return get latest fetch
     */
    @SuppressWarnings("rawtypes")
    protected FetchParent getLatestFetch(Root from, Map<String, FetchParent> fetchs, String[] strings) {
        // /not implemented yet

        FetchParent fetch = from;
        for (int j = 0; j < strings.length; j++) {
            String propertyName = strings[j];
            String key = keyMaker(strings, j);

            if (fetchs.containsKey(key)) {
                fetch = fetchs.get(key);

            } else {
                if (j == 0) {
                    fetch = from.fetch(propertyName);
                } else {
                    fetch = fetch.fetch(propertyName);
                }
                fetchs.put(key, fetch);
            }

        }
        return fetch;
    }

    /**
     * create key from seperated key array
     *
     * @param strings key array seperated by .
     * @param j       last index of proerty
     * @return key
     */
    protected String keyMaker(String[] strings, int j) {
        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0; i <= j; i++) {
            String string = strings[i];
            if (i > 0)
                stringBuilder.append(".");
            stringBuilder.append(string);

        }
        return stringBuilder.toString();
    }

    public void detach(Object entity) {
        getEntityManager().detach(entity);

    }

    public Object getId(Object entity) {
        return getEntityManager().getEntityManagerFactory().getPersistenceUnitUtil().getIdentifier(entity);
    }

    public void flush() {
        getEntityManager().flush();
    }

    public SearchResultList query(Class objectClass, YQuery yQuery, YPage paging) {
          StringBuffer afterFrom=new StringBuffer();
          StringBuffer fetchs=new StringBuffer();
          StringBuffer whereClause=new StringBuffer();
          StringBuffer orderClause=new StringBuffer();
        final String baseClassName = objectClass.getSimpleName();

        StringBuffer select=new StringBuffer();
        if(Utils.isEmpty(yQuery.getSelect())){
            select.append("select ").append("this").append(" ");
        }else{
            select.append("select ").append(yQuery.getSelect()).append(" ");

        }

        afterFrom.append(" from ").append(baseClassName).append(" this");
        String[] daoFetchs=yQuery.getFetchProperties();
        if(daoFetchs!=null){
            for (String fetch : daoFetchs) {
                fetchs.append(" left join fetch this.").append(fetch).append(" ");
            }
        }
        boolean where =false;
        if(Utils.isNotEmpty(yQuery.getDefaultWhere())){
            where=true;
            whereClause.append(" where ").append(yQuery.getDefaultWhere());
        }
        List<Object> parameters=new ArrayList<Object>();

        List<YCriteria> yCriterias = yQuery.getCriterias();

        List<YCriteria> list = yCriterias;
        for (YCriteria yCriteria : list) {
            if (where)
                whereClause.append(" and ");
            else {
                whereClause.append(" where ");
                where = true;
            }
            whereClause.append(yCriteria.getCriteria());
        }
        List<YOrder> yOrderList = paging.getOrders();
        if(yOrderList!=null&&!yOrderList.isEmpty()){
            orderClause.append(" order by ");
            for (YOrder order : yOrderList) {
                orderClause.append(" this.").append(order.getName()).append(" ").append(order.getSort().toString().toLowerCase());
            }
        }
        StringBuffer countSelect = new StringBuffer("select count(*) ").append(afterFrom.toString()).append(whereClause.toString());
        select.append(afterFrom.toString()).append(fetchs.toString()).append(whereClause.toString()).append(orderClause);
        List resultList= (List) executeQuery(yQuery, select, list,true,paging);
        Number count= (Number) executeQuery(yQuery, countSelect, list,false,paging);
        return new SearchResultList(resultList,count.longValue(),paging);

    }

    private Object executeQuery(YQuery yQuery, StringBuffer select, List<YCriteria> list, boolean isList, YPage paging) {
        Query query = getEntityManager().createQuery(select.toString());
        int index=1;
        Object[] objects = yQuery.getDefaultArgs();
        if (objects != null) {
            for (Object value : objects) {
                query.setParameter(index, value);
                index++;
            }
        }
        for (YCriteria yCriteria : list) {
            Object[] values = yCriteria.getValues();
            if(values==null)
                continue;
            for (Object value : values) {
                query.setParameter(index,value);
                index++;
            }
        }
        if(isList){
            setLimits(query, paging);
            return query.getResultList();
        }
        return query.getSingleResult();
    }


}
